/*
 * BlackLightning.PluginLib.Modules.Packet
 * Andy Tidball
 * 
 * Project: Black Lightning Plugin Library
 * Copyright: GNU General Public License
 */

using System;
using System.Text;
using System.Text.RegularExpressions;
using BLLib.Net;

namespace BlackLightning.PluginLib.Modules {
	/// <summary>
	/// Abstract base class for packets which are travelling through the module chain from source to destination.
	/// </summary>
	[Serializable]
	public abstract class BLPacket : Packet, ICloneable {
		// member data
		private Program  _Source;       // the program that the packet came from
		private Program  _Destination;  // the program that the packet will be delivered to
		private BLPacket _Previous;     // a reference to this packet's previous state
		private int      _States;       // the number of previous states this packet has held during processing
		private bool     _Suppressed;   // whether or not the packet has been suppressesd from delivery

		/// <summary>
		/// Creates a new BLPacket.
		/// </summary>
		/// <param name="Source">The program that the packet came from.</param>
		/// <param name="Destination">The program that the packet will be delivered to.</param>
		/// <param name="Data">The data string contained in the packet.</param>
		/// <param name="Context">Contextual information to use to parse information from the given data.</param>
		internal BLPacket(Program Source, Program Destination, string Data, BLPacketContext Context)
			: base(Data, Context) {
			_Source      = Source;
			_Destination = Destination;
			_Previous    = null;
			_States      = 1;
			_Suppressed  = false;
		}

		/// <summary>
		/// Copy constructor.
		/// </summary>
		/// <param name="P">The BLPacket to create a copy of.</param>
		/// <param name="DeepCopyOrNextState">True to make a deep copy of the given packet, false to instead use the given packet as the new packet's previous state.</param>
		internal BLPacket(BLPacket P, bool DeepCopyOrNextState)
			: base(P) {
			_Source       = P._Source;
			_Destination  = P._Destination;
			_Suppressed   = P._Suppressed;

			if (DeepCopyOrNextState) {
				_Previous = P._Previous == null ? null : P._Previous.Clone() as BLPacket;
				_States   = P._States;
			} else {
				_Previous = P;
				_States   = P._States + 1;
			}
		}

		/// <summary>
		/// Gets the program that the packet came from.
		/// </summary>
		public Program Source {
			get {
				return _Source;
			}
		}

		/// <summary>
		/// Gets the program that the packet will be delivered to.
		/// </summary>
		public Program Destination {
			get {
				return _Destination;
			}
		}

		/// <summary>
		/// Gets the packet's state before being handled by the current Module.
		/// </summary>
		public BLPacket PreviousState {
			get {
				return _Previous;
			}
		}

		/// <summary>
		/// The number of previous states this packet has held while being processed.
		/// </summary>
		public int PreviousStates {
			get {
				return _States;
			}
		}

		/// <summary>
		/// Whether or not this packet has been suppressed from further processing and delivery.
		/// </summary>
		public bool Suppressed {
			get {
				return _Suppressed;
			}
		}

		/// <summary>
		/// Suppresses the packet from any further processing by modules and from delivery to its destination.  The packet is effectively dropped.
		/// </summary>
		public void Suppress() {
			_Suppressed = true;
		}

		/// <summary>
		/// Creates a deep copy of the packet.
		/// </summary>
		/// <returns>A deep copy of the packet.</returns>
		public abstract object Clone();

		/// <summary>
		/// Creates a new Packet with this one being its PreviousState.
		/// </summary>
		/// <returns>A new Packet that represents the next state of this one.</returns>
		internal abstract BLPacket GetNextState();
	}

	/// <summary>
	/// A packet which doesn't break the data string down into any more detail.
	/// </summary>
	[Serializable]
	public class RawBLPacket : BLPacket {
		// member data
		private string _Data; // the packet's data

		/// <summary>
		/// Creates a new RawBLPacket.
		/// </summary>
		/// <param name="Source">The program that the packet came from.</param>
		/// <param name="Destination">The program that the packet will be delivered to.</param>
		/// <param name="Data">The data string contained in the packet.</param>
		public RawBLPacket(Program Source, Program Destination, string Data)
			: this(Source, Destination, Data, null) {
		}

		/// <summary>
		/// Creates a new RawBLPacket.
		/// </summary>
		/// <param name="Source">The program that the packet came from.</param>
		/// <param name="Destination">The program that the packet will be delivered to.</param>
		/// <param name="Data">The data string contained in the packet.</param>
		/// <param name="Context">Contextual information to use to parse information from the given data.</param>
		internal RawBLPacket(Program Source, Program Destination, string Data, BLPacketContext Context)
			: base(Source, Destination, Data, Context) {
			_Data = Data;
		}

		/// <summary>
		/// Copy constructor.
		/// </summary>
		/// <param name="P">The RawBLPacket to create a copy of.</param>
		/// <param name="DeepCopyOrNextState">True to make a deep copy of the given packet, false to instead use the given packet as the new packet's previous state.</param>
		internal RawBLPacket(RawBLPacket P, bool DeepCopyOrNextState)
			: base(P, DeepCopyOrNextState) {
			_Data = P._Data;
		}

		/// <summary>
		/// The data contained in the packet.
		/// </summary>
		public string Data {
			get {
				return _Data;
			}
			set {
				_Data = value;
			}
		}

		/// <summary>
		/// Retrieves the packet's data.
		/// </summary>
		/// <param name="Context">Contextual information to use to create output from the packet's data.</param>
		/// <returns>The packet's data string.</returns>
		public override string CreateOutput(PacketContext Context) {
			return _Data;
		}

		/// <summary>
		/// Creates a deep copy of the packet.
		/// </summary>
		/// <returns>A deep copy of the packet.</returns>
		public override object Clone() {
			return new RawBLPacket(this, true);
		}
		
		/// <summary>
		/// Creates a new RawBLPacket that has this RawBLPacket as its PreviousState.
		/// </summary>
		/// <returns>A new RawBLPacket that has this one as its PreviousState.</returns>
		internal override BLPacket GetNextState() {
			return new RawBLPacket(this, false);
		}
	}

	/// <summary>
	/// A packet sent to the front end to display an error.
	/// </summary>
	[Serializable]
	public class ExceptionPacket : RawBLPacket {
		/// <summary>
		/// Formats the given exception into a string that's nice for display.
		/// </summary>
		/// <param name="x">The exception to format.</param>
		/// <returns>A nicely formatted string that represents the exception.</returns>
		private static string FormatException(Exception x) {
			StringBuilder Output = new StringBuilder("An exception has occurred." + Environment.NewLine);
			string Indent = "";

			// if this is a MemberAccessException, we can output some slightly more useful information
			if (x is MemberAccessException) {
				Output.Append("This exception probably occurred because a module or plugin tried to use functionality that only exists in later versions of Black Lightning." + Environment.NewLine);
				Output.Append("Check to make sure that the version of the plugin you're using is compatible with your current version of Black Lightning." + Environment.NewLine);
				Output.Append("You can use the '@about' and '@about [plugin]' commands for version information about Black Lightning and the plugin in question, respectively." + Environment.NewLine);
				Output.Append("It is recommended that you unload the module and/or plugin that caused this exception, because it may no longer be in a stable state." + Environment.NewLine);
			}

			// loop through the exception and all its inner exceptions
			while (x != null) {
				// split the stack into lines
				string Stack = x.StackTrace.Replace(Environment.NewLine, "\n");
				string[] Split = Stack.Split('\n');

				// find the first function call on the stack
				int SplitIndex = 0;
				while (!Split[SplitIndex].StartsWith("  ")) {
					++SplitIndex;
				}

				// parse out the method and filename/line from the first line
				int AtIndex  = Split[SplitIndex].IndexOf("at ");
				int InIndex  = Split[SplitIndex].IndexOf(" in ");
				string Method   = "";
				string FileLine = "";
				if (InIndex >= 0) {
					Method   = Split[SplitIndex].Substring(AtIndex + "at ".Length, InIndex - AtIndex - "at ".Length);
					FileLine = Split[SplitIndex].Substring(InIndex + " in ".Length);
				} else {
					Method   = Split[SplitIndex].Substring(AtIndex + "at ".Length);
					FileLine = "Unknown";
				}

				// output the data for this exception
				Output.Append(Indent + "Type: " + x.GetType().Name + Environment.NewLine);
				Output.Append(Indent + "Message: " + x.Message + Environment.NewLine);
				Output.Append(Indent + "Method: " + Method + Environment.NewLine);
				Output.Append(Indent + "Location: " + FileLine + Environment.NewLine);
				//Output.Append(Indent + "Stack: " + x.StackTrace + Environment.NewLine);
				if (x.InnerException != null) {
					Output.Append(Indent + "Inner Exception:" + Environment.NewLine);
				}

				// move to the next inner exception and up the indent level
				x = x.InnerException;
				Indent += "    ";
			}

			// return the message
			return Output.ToString();
		}

		/// <summary>
		/// Formats only the message of the given exception and its inner exceptions.
		/// </summary>
		/// <param name="x">The exception to format the message(s) of.</param>
		/// <returns>A nicely formatted string containing the exception's message(s).</returns>
		private static string FormatExceptionMessages(Exception x) {
			StringBuilder Output = new StringBuilder("An error has occurred.");
			string Indent = "";
			while (x != null) {
				Output.Append(Environment.NewLine);
				Output.Append(Indent);
				Output.Append(x.Message);
				Indent += "    ";
				x = x.InnerException;
			}
			return Output.ToString();
		}

		// member data
		private Exception _Exception = null; // the exception to dispay in the front end

		/// <summary>
		/// Creates a new ExceptionPacket.
		/// </summary>
		/// <param name="x">The exception to format and send to the front end.</param>
		public ExceptionPacket(Exception x)
			: this(x, false) {
		}

		/// <summary>
		/// Creates a new ExceptionPacket.
		/// </summary>
		/// <param name="x">The exception to format and send to the front end.</param>
		/// <param name="MessagesOnly">Whether or not to only include the only exception messages, or to include other data as well.</param>
		public ExceptionPacket(Exception x, bool MessagesOnly)
			: base(Program.BlackLightning, Program.FrontEnd, (MessagesOnly ? FormatExceptionMessages(x) : FormatException(x))) {
			_Exception = x;
		}

		/// <summary>
		/// Gets the exception that this packet is formatting and sending.
		/// </summary>
		public Exception Exception {
			get {
				return _Exception;
			}
		}
	}

	/// <summary>
	/// A filter that can be matched against BLPackets.
	/// </summary>
	[Serializable]
	public class BLPacketFilter {
		// member data
		private string    _RawInputString = null; // the string to match against the packet's raw input
		private Regex     _RawInputRegex  = null; // the regular expression to match against the packet's raw input
		private Program?  _Source         = null; // the source program to match against the packet's source
		private Program?  _Destination    = null; // the destination program to match against the packet's destination
		private DateTime? _StartTime      = null; // the datetime that the packet's datetime must be later than or equal to
		private DateTime? _EndTime        = null; // the datetime that the packet's datetime must be earlier than or equal to

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		public BLPacketFilter() {
		}

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		/// <param name="Input">The input string to match against the packet's raw input.</param>
		public BLPacketFilter(string Input) {
			_RawInputString = Input;
		}

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		/// <param name="Input">The regular expression to match against the packet's raw input.</param>
		public BLPacketFilter(Regex Input) {
			_RawInputRegex  = Input;
		}

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		public BLPacketFilter(Program Source, Program Destination) {
			_Source         = Source;
			_Destination    = Destination;
		}

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		/// <param name="Input">The input string to match against the packet's raw input.</param>
		public BLPacketFilter(Program Source, Program Destination, string Input) {
			_RawInputString = Input;
			_Source         = Source;
			_Destination    = Destination;
		}

		/// <summary>
		/// Creates a new BLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		/// <param name="Input">The regular expression to match against the packet's raw input.</param>
		public BLPacketFilter(Program Source, Program Destination, Regex Input) {
			_RawInputRegex  = Input;
			_Source         = Source;
			_Destination    = Destination;
		}

		/// <summary>
		/// Gets or sets the string which is matched against the packet's raw input string.
		/// </summary>
		public string RawInputString {
			get {
				return _RawInputString;
			}
			set {
				_RawInputString = value;
			}
		}

		/// <summary>
		/// Gets or sets the regular expression which is matched against the packet's raw input string.
		/// </summary>
		public Regex RawInputRegex {
			get {
				return _RawInputRegex;
			}
			set {
				_RawInputRegex = value;
			}
		}

		/// <summary>
		/// Gets or sets the program which is matched against the packet's source program.
		/// </summary>
		public Program? Source {
			get {
				return _Source;
			}
			set {
				_Source = value;
			}
		}

		/// <summary>
		/// Gets or sets the program which is matched against the packet's destination program.
		/// </summary>
		public Program? Destination {
			get {
				return _Destination;
			}
			set {
				_Destination = value;
			}
		}

		/// <summary>
		/// Gets or sets the time that the packet's creation time must be later than or equal to.
		/// </summary>
		public DateTime? StartTime {
			get {
				return _StartTime;
			}
			set {
				_StartTime = value;
			}
		}

		/// <summary>
		/// Gets or sets the time that the packet's creation time must be earlier than or equal to.
		/// </summary>
		public DateTime? EndTime {
			get {
				return _EndTime;
			}
			set {
				_EndTime = value;
			}
		}

		/// <summary>
		/// Sets the Source and Destination properties with one method call.
		/// </summary>
		/// <param name="Source">The source program to match against packet's source programs.</param>
		/// <param name="Destination">The destination program to match against packet's destination programs.</param>
		public void SetPrograms(Program? Source, Program? Destination) {
			_Source      = Source;
			_Destination = Destination;
		}

		/// <summary>
		/// Sets the StartTime and EndTime properties with one method call.
		/// </summary>
		/// <param name="StartTime">The time that matched packet's creation time must be later than or equal to.</param>
		/// <param name="EndTime">The time that matched packet's creation time must be earlier than or equal to.</param>
		public void SetCreationTime(DateTime? StartTime, DateTime? EndTime) {
			_StartTime = StartTime;
			_EndTime   = EndTime;
		}

		/// <summary>
		/// Matches the given packet's static data against the filter.
		/// </summary>
		/// <param name="Packet">The packet to match against the filter.</param>
		/// <returns>True if the packet's static data matches the current setup of the filter, false if it does not.</returns>
		public virtual bool StaticMatch(BLPacket Packet) {
			// match the source program
			if (this.Source != null && Packet.Source != this.Source) {
				return false;
			}

			// match the destination program
			if (this.Destination != null && Packet.Destination != this.Destination) {
				return false;
			}

			// match the start time
			if (this.StartTime != null && Packet.CreationTime < this.StartTime) {
				return false;
			}

			// match the end time
			if (this.EndTime != null && Packet.CreationTime > this.EndTime) {
				return false;
			}

			// everything passed
			return true;
		}

		/// <summary>
		/// Matches the given packet's dynamic data against the filter.
		/// </summary>
		/// <param name="Packet">The packet to match against the filter.</param>
		/// <returns>True if the packet's dynamic data matches the current setup of the filter, false if it does not.</returns>
		public virtual bool DynamicMatch(BLPacket Packet) {
			// match the raw input string
			if (this.RawInputString != null && Packet.RawInput != this.RawInputString) {
				return false;
			}

			// match the raw input regular expression
			// Regexes seem to use \n for newlines rather than Environment.NewLine
			// Since we're using the latter, do a replacement before matching
			if (this.RawInputRegex != null && !this.RawInputRegex.IsMatch(Packet.RawInput.Replace(Environment.NewLine, "\n"))) {
				return false;
			}

			// everything passed
			return true;
		}
	}

	/// <summary>
	/// A BLPacketFilter to use with RawBLPackets.
	/// </summary>
	[Serializable]
	public class RawBLPacketFilter : BLPacketFilter {
		// member data
		private string _DataString = null; // the string to match against the packet's data
		private Regex  _DataRegex  = null; // the regular expression to match against the packet's data

		/// <summary>
		/// Creates a new RawBLPacketFilter.
		/// </summary>
		public RawBLPacketFilter() {
		}

		/// <summary>
		/// Creates a new RawBLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		public RawBLPacketFilter(Program Source, Program Destination)
			: base(Source, Destination) {
		}

		/// <summary>
		/// Creates a new RawBLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		/// <param name="Input">The input string to match against the packet's raw input.</param>
		public RawBLPacketFilter(Program Source, Program Destination, string Input)
			: base(Source, Destination) {
			_DataString = Input;
		}

		/// <summary>
		/// Creates a new RawBLPacketFilter.
		/// </summary>
		/// <param name="Source">The source program to match against the packet's source.</param>
		/// <param name="Destination">The destination program to match against the packet's destination.</param>
		/// <param name="Input">The regular expression to match against the packet's raw input.</param>
		public RawBLPacketFilter(Program Source, Program Destination, Regex Input)
			: base(Source, Destination) {
			_DataRegex = Input;
		}

		/// <summary>
		/// The string to match against the packet's data string.
		/// </summary>
		public string DataString {
			get {
				return _DataString;
			}
			set {
				_DataString = value;
			}
		}

		/// <summary>
		/// The regular expression to match against the packet's data string.
		/// </summary>
		public Regex DataRegex {
			get {
				return _DataRegex;
			}
			set {
				_DataRegex = value;
			}
		}

		/// <summary>
		/// Matches the packet's dynamic data against the filter.
		/// </summary>
		/// <param name="Packet">The packet to match against the filter.</param>
		/// <returns>True if the packet's dynamic data matches the current setup of the filter, false if it does not.</returns>
		public override bool DynamicMatch(BLPacket Packet) {
			// make sure this is a RawBLPacket
			RawBLPacket P = Packet as RawBLPacket;
			if (P == null) {
				return false;
			}

			// match the data string
			if (this.DataString != null && P.Data != this.DataString) {
				return false;
			}

			// match the data regular expression
			// Regexes seem to use \n for newlines rather than Environment.NewLine
			// Since we're using the latter, do a replacement before matching
			if (this.DataRegex != null && !this.DataRegex.IsMatch(P.Data.Replace(Environment.NewLine, "\n"))) {
				return false;
			}

			// everything passed, match the base packet data
			return base.DynamicMatch(P);
		}
	}

	/// <summary>
	/// The contextual information used to parse and re-form BLPacket data.
	/// </summary>
	public class BLPacketContext : PacketContext {
		// member data
		private Protocol _Protocol; // the to use to transfer between a data string and the packet's data

		/// <summary>
		/// Creates a new BLPacketContext.
		/// </summary>
		/// <param name="Protocol">The protocol to include in the contextual information.</param>
		public BLPacketContext(Protocol Protocol) {
			_Protocol = Protocol;
		}

		/// <summary>
		/// Gets the protocol included in the contextual information.
		/// </summary>
		public Protocol Protocol {
			get {
				return _Protocol;
			}
		}
	}

	/// <summary>
	/// An enumeration of all programs involved with BlackLightning.
	/// </summary>
	public enum Program {
		/// <summary>
		/// The Black Lightning program.
		/// </summary>
		BlackLightning,

		/// <summary>
		/// The game server program that Black Lightning is connected to.
		/// </summary>
		GameServer,

		/// <summary>
		/// The front end program that Black Lightning is connected to.
		/// </summary>
		FrontEnd
	}
}
